﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Reflection;
using DynamicMethodResolver.Exceptions;
using System.ServiceModel;

namespace DynamicMethodResolver
{
    /// <summary>
    /// Determines how to dig out the method from the specified class
    /// </summary>
    public enum MethodResolvingMode
    {
        /// <summary>
        /// Returns the method only if the method is having exact signature match
        /// </summary>
        ExactMatch = 0,

        /// <summary>
        /// Returns the best match for the method. Will return atleast a single method if such a method name exists.
        /// </summary>
        BestMatch = 1
    }

    /// <summary>
    /// Dynamic Method Resolver class will find the suitable method among methods based on the criteria passed and invokes the method with the arguments passed.
    /// </summary>
    public class DMR
    {
        #region Public Methods

        /// <summary>
        /// Finds and executes the method based on the criteria passed.
        /// </summary>
        /// <param name="criteria">Contains information related with the Class, the Method and its Parameters.</param>
        /// <returns>Returns the result after finding and executing the original method.</returns>
        public static object Execute(Criteria criteria)
        {
            return Execute(criteria, MethodResolvingMode.ExactMatch);
        }

        /// <summary>
        /// Finds and executes the method based on the criteria passed and Method Resolving Mode.
        /// </summary>
        /// <param name="criteria">Contains information related with the Class, the Method and its Parameters.</param>
        /// <param name="mode">ExactMatch : wil return a method if the signature of the method matches perfectly with that of the criteria. BestMatch : Will try to find out a suitable overload.</param>
        /// <returns>Returns the result after finding and executing the original method.</returns>
        public static object Execute(Criteria criteria, MethodResolvingMode mode)
        {

            return Execute(criteria, Assembly.GetCallingAssembly(), mode);
        }

        /// <summary>
        /// Finds and executes the method based on the criteria passed and Method Resolving Mode. 
        /// </summary>
        /// <param name="criteria">Contains information related with the Class, the Method and its Parameters.</param>
        /// <param name="assembly">Gets the assembly in which the Resolver should look for the class and the method.</param>
        /// <param name="mode">ExactMatch : wil return a method if the signature of the method matches perfectly with that of the criteria. BestMatch : Will try to find out a suitable overload.</param>
        /// <returns>Returns the result after finding and executing the original method.</returns>
        public static object Execute(Criteria criteria, Assembly assembly, MethodResolvingMode mode)
        {
            IEnumerable<Type> types = from Type t in assembly.GetTypes() where t.Name.Contains(criteria.Entity) select t;
            object obj = Activator.CreateInstance(types.ToArray<Type>()[0]);

            MethodInfo method = TryGetMethod(obj.GetType(), criteria, mode);

            if (method == null)
            {
                MethodNotFoundException ex = new MethodNotFoundException() { Reason = "Method Not Found!!!" };
                FaultReason reason = new FaultReason(ex.Reason);
                throw new FaultException<MethodNotFoundException>(ex,reason);
            }
            try
            {
                return method.Invoke(obj, GetParametersForMethod(method, criteria));
            }
            catch (Exception ex)
            {
                MethodExecutionFailedException exf = new MethodExecutionFailedException() { Reason = "Method Execution Failed : " + ex.Message, StackTrace = ex.StackTrace };
                FaultReason reason = new FaultReason(exf.Reason);
                throw new FaultException<MethodExecutionFailedException>(exf,reason);
            }
        }

        #endregion

        #region Private Methods

        private static MethodInfo TryGetMethod(Type type, Criteria criteria, MethodResolvingMode mode)
        {
            IEnumerable<Type> types = GetMethodParameterTypes(criteria.Parameters);
            MethodInfo method = type.GetMethod(criteria.Method, types.ToArray<Type>());
            IEnumerable<MethodInfo> methods;

            switch (mode)
            {
                case MethodResolvingMode.ExactMatch:
                    {
                        break;
                    }

                case MethodResolvingMode.BestMatch:
                    {
                        if (method == null)
                        {
                            methods = GetMethodsWithSameName(type, criteria.Method);

                            if (methods.Count() != 1)
                            {
                                method = FindBestMethod(methods, types, criteria.Parameters);
                            }

                            if (method == null && methods.Count() > 0)
                            {
                                method = methods.FirstOrDefault();
                            }
                        }
                        break;
                    }
            }

            return method;
        }

        private static MethodInfo FindBestMethod(IEnumerable<MethodInfo> methods, IEnumerable<Type> types, Dictionary<string, object> dictParameters)
        {
            Int32 paramMatchCount;
            Int32 prevParamMatchCount = 0;
            MethodInfo method = null;
            Dictionary<Type, Int32> typeCountDict = new Dictionary<Type, int>();

            foreach (Type t in types)
            {
                if (typeCountDict.Keys.Contains(t))
                {
                    typeCountDict[t] += 1;
                }
                else
                {
                    typeCountDict.Add(t, 1);
                }
            }

            foreach (MethodInfo mi in methods)
            {
                paramMatchCount = 0;
                foreach (Type t in typeCountDict.Keys)
                {
                    IEnumerable<ParameterInfo> parameters = from ParameterInfo pi in mi.GetParameters()
                                                            where pi.ParameterType == t && dictParameters.Keys.Contains<string>(pi.Name)
                                                            select pi;
                    if (parameters.Count() == typeCountDict[t])
                    {
                        paramMatchCount += parameters.Count();
                    }
                }

                if (paramMatchCount == types.Count())
                {
                    method = mi;
                    break;
                }

                if (paramMatchCount >= prevParamMatchCount)
                {
                    method = mi;
                    prevParamMatchCount = paramMatchCount;
                }
            }

            return method;
        }

        private static IEnumerable<MethodInfo> GetMethodsWithSameName(Type type, string methodName)
        {
            IEnumerable<MethodInfo> methods = from MethodInfo mi in type.GetMethods()
                                              where mi.Name == methodName
                                              select mi;
            return methods;
        }

        private static IEnumerable<Type> GetMethodParameterTypes(Dictionary<string, object> Parameters)
        {
            List<Type> types = new List<Type>();
            foreach (object obj in Parameters.Values)
            {
                types.Add(obj.GetType());
            }
            return types;
        }

        private static object[] GetParametersForMethod(MethodInfo method, Criteria criteria)
        {
            List<object> parameters = new List<object>();

            foreach (ParameterInfo pi in method.GetParameters())
            {
                if (criteria.Parameters.ContainsKey(pi.Name))
                {
                    parameters.Add(criteria.Parameters[pi.Name]);
                }
                else
                {
                    parameters.Add(null);
                }
            }

            return parameters.ToArray();
        }

        #endregion
    }
}
